<!doctype html><html lang=cn-zh><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><meta http-equiv=X-UA-Compatible content="IE=edge"><style type=text/css>body{font-family:monospace}</style><title>TLPI笔记—通用文件I/O模型</title>
<meta name=description content="A blog maintained by Vimiix."><link rel=stylesheet href=/css/style.css><script>var _hmt=_hmt||[];(function(){var e,t=document.createElement("script");t.src="https://hm.baidu.com/hm.js?7c24231917964240bae97e813810620c",e=document.getElementsByTagName("script")[0],e.parentNode.insertBefore(t,e)})()</script></head><body><header>====================<br>== Hi, I'm Vimiix ==<br>====================<div style=float:right;color:gray;font-size:x-large>Get hands dirty.</div><br><p><nav><a href=https://www.vimiix.com/><b>首页</b></a>.
<a href=/posts/><b>文章列表</b></a>.
<a href=/projects/><b>开源项目</b></a>.
<a href=/tags/><b>标签</b></a>.
<a href=/friends/><b>友链</b></a>.
<a href=/about/><b>关于我</b></a>.
<a href=/index.xml><b>RSS</b></a>.</nav></p></header><main><article><h1>TLPI笔记—通用文件I/O模型</h1><b><time>2018.05.11 09:20</time></b>
<a href=/tags/tlpi>tlpi</a>
<a href=/tags/linux>Linux</a>
<a href=/tags/note>note</a>
<a href=/tags/i/o>I/O</a><div><h2 id=文件描述符>文件描述符</h2><p>所有执行 I/O 操作的系统调用都是以文件描述符，一个非负整数来指代打开的文件。文件描述符用以表示所有类型的已打开的文件，包括管道（pipe）、FIFO、socket、终端、设备和普通文件。每个进程都各自独立维护着一张文件描述符表。</p><h3 id=标准文件描述符>标准文件描述符</h3><table><thead><tr><th style=text-align:center>文件描述符</th><th style=text-align:center>用途</th><th style=text-align:center>POSIX 名称</th><th style=text-align:center>stdio 流</th></tr></thead><tbody><tr><td style=text-align:center>0</td><td style=text-align:center>标准输入</td><td style=text-align:center>STDIN_FILENO</td><td style=text-align:center>stdin</td></tr><tr><td style=text-align:center>1</td><td style=text-align:center>标准输出</td><td style=text-align:center>STDOUT_FILENO</td><td style=text-align:center>stdout</td></tr><tr><td style=text-align:center>2</td><td style=text-align:center>标准错误</td><td style=text-align:center>STDERR_FILENO</td><td style=text-align:center>stderr</td></tr></tbody></table><h2 id=通用-io>通用 I/O</h2><p>UNIX I/O 模型的限制特点之一是其输入/输出的通用性概念。这意味着使用 4 个同样的系统调用<code>open()</code>，<code>resd()</code>，<code>write()</code>，<code>close()</code> 可以对所有类型的文件执行 I/O 操作，包括终端之类的设备。因此，仅使用这些系统调用编写的程序，将对任何类型的文件都有效。</p><p>要实现通用 I/O，就必须确保每一文件系统和设备驱动程序都实现了相同的 I/O 系统调用集。由于文件系统和设备所特有的操作细节在内核中处理，在编程时通常可以忽略设备转悠的因素。一旦应用程序需要访问文件系统或设备的专有功能时，可以选择强大的<a href=http://pubs.opengroup.org/onlinepubs/009604599/functions/ioctl.html><code>ioctl()</code></a>系统调用，该调用为通用 I/O 模型之外的专有特性提供了访问接口。</p><h2 id=打开一个文件open>打开一个文件：open()</h2><p><code>open()</code>调用既可以打开一个已经存在的文件，也可以创建并打开一个新的文件。如果调用成功返回指代所打开的文件的文件描述符，若发生错误，则返回-1。</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-c data-lang=c><span style=display:flex><span><span style=color:#75715e>#include</span> <span style=color:#75715e>&lt;sys/stat.h&gt;</span><span style=color:#75715e>
</span></span></span><span style=display:flex><span><span style=color:#75715e>#include</span> <span style=color:#75715e>&lt;fcntl.h&gt;</span><span style=color:#75715e>
</span></span></span><span style=display:flex><span><span style=color:#75715e></span>
</span></span><span style=display:flex><span><span style=color:#66d9ef>int</span> <span style=color:#a6e22e>open</span>(<span style=color:#66d9ef>const</span> <span style=color:#66d9ef>char</span> <span style=color:#f92672>*</span>pathname, <span style=color:#66d9ef>int</span> flags, ...<span style=color:#75715e>/* mode_t mode*/</span>);
</span></span><span style=display:flex><span><span style=color:#75715e>// Return file description on success, or -1 in error
</span></span></span></code></pre></div><ul><li>pathname：要打开的文件，如果 pathname 是一个符号链接，会对其进行解引用。</li><li>flags： 位掩码参数，用户指定文件的访问模式，后面给出常用访问模式列表</li><li>mode：位掩码参数，指定了文件的访问权限（如果访问模式未指定 O_CREATE 标志，则可以省略 mode 参数）</li></ul><h3 id=常用-flags文件访问模式参数>常用 flags（文件访问模式）参数</h3><table><thead><tr><th style=text-align:left>标志</th><th style=text-align:left>用途</th><th style=text-align:center>统一 UNIX 规范版本</th></tr></thead><tbody><tr><td style=text-align:left>O_RDONLY</td><td style=text-align:left>以只读方式打开</td><td style=text-align:center>v3</td></tr><tr><td style=text-align:left>O_WRONLY</td><td style=text-align:left>以只写方式打开</td><td style=text-align:center>v3</td></tr><tr><td style=text-align:left>O_RDWR</td><td style=text-align:left>以读写方式打开</td><td style=text-align:center>v3</td></tr><tr><td style=text-align:left>O_CLOEXEC</td><td style=text-align:left>设置 <code>close-on-exec</code>标志(Linux 2.6.23+)</td><td style=text-align:center>v4</td></tr><tr><td style=text-align:left>O_CREATE</td><td style=text-align:left>若文件不存在则创建</td><td style=text-align:center>v3</td></tr><tr><td style=text-align:left>O_DIRECT</td><td style=text-align:left>无缓冲的输入输出</td><td style=text-align:center></td></tr><tr><td style=text-align:left>O_DIRECTORY</td><td style=text-align:left>如果 pathname 不是目录，则失败</td><td style=text-align:center></td></tr><tr><td style=text-align:left>O_EXCL</td><td style=text-align:left>结合 O_CREATE 参数使用，确保调用者以独占方式访问文件</td><td style=text-align:center>v4</td></tr><tr><td style=text-align:left>O_LARGEFILE</td><td style=text-align:left>在 32 位系统中使用此标志打开大文件</td><td style=text-align:center></td></tr><tr><td style=text-align:left>O_NOATIME</td><td style=text-align:left>调用<code>read()</code>时，不修改文件最近访问时间(Linux2.6.8+)</td><td style=text-align:center></td></tr><tr><td style=text-align:left>O_NOCTTY</td><td style=text-align:left>不让 pathname 所指向的终端设备成为控制终端</td><td style=text-align:center>v3</td></tr><tr><td style=text-align:left>O_NOFOLLOW</td><td style=text-align:left>对符号链接不予解引用</td><td style=text-align:center>v4</td></tr><tr><td style=text-align:left>O_TRUNC</td><td style=text-align:left>截断已有文件，使其长度为 0</td><td style=text-align:center>v3</td></tr><tr><td style=text-align:left>O_APPEND</td><td style=text-align:left>总在文件尾部追加数据</td><td style=text-align:center>v3</td></tr><tr><td style=text-align:left>O_ASYNC</td><td style=text-align:left>当 I/O 操作可行时，产生信号通知进程（信号驱动 I/O）</td><td style=text-align:center></td></tr><tr><td style=text-align:left>O_DSYNC</td><td style=text-align:left>提供同步的 I/O 数据完整性(Linux 2.6.33+)</td><td style=text-align:center>v3</td></tr><tr><td style=text-align:left>O_NONBLOCK</td><td style=text-align:left>以非阻塞方式打开</td><td style=text-align:center>v3</td></tr><tr><td style=text-align:left>O_SYNC</td><td style=text-align:left>以同步方式写入文件</td><td style=text-align:center>v3</td></tr></tbody></table><h3 id=open函数的错误>open()函数的错误</h3><p>若打开文件时发生错误，<code>open()</code>将返回-1，错误号<code>errno</code>标识错误原因。以下是一些可能发生的错误：</p><table><thead><tr><th>errno</th><th style=text-align:left>描述</th></tr></thead><tbody><tr><td>EACCES</td><td style=text-align:left>文件权限不允许调用进程以 flags 参数指定的方式开大文件。无法访问文件，其可能原因有目录权限的限制、文件不存在并且无法创建该文件。</td></tr><tr><td>EISDIR</td><td style=text-align:left>所指定的文件属于目录，而调用者企图打开该文件进行写操作。不允许这种用法。（<em>另一方面，在某些场合中，打开目录进行读操作是有必要的。</em>）</td></tr><tr><td>EMFILE</td><td style=text-align:left>进程已打开的文件描述符数量达到了进程资源限制所设定的上限（<em>RLIMIT_NOFILE 参数</em>）</td></tr><tr><td>ENFILE</td><td style=text-align:left>文件打开数量已经达到系统允许的上限</td></tr><tr><td>ENOENT</td><td style=text-align:left>要么文件不存在且未指定 O_CREATE 标志，要么指定了 O_CREATE 标志，但 pathname 参数所指定路径的目录之一不存在，或者 pathname 参数为符号链接，而该链接指向的文件不存在</td></tr><tr><td>EROFS</td><td style=text-align:left>所指定的文件隶属于只读文件系统，而调用者企图以写方式打开文件</td></tr><tr><td>ETXTBSY</td><td style=text-align:left>所指定的文件为可执行文件，且正在运行中。系统不允许修改正在运行的程序。必须先终止程序运行，然后方可修改可执行文件。</td></tr></tbody></table><h2 id=读取文件内容read>读取文件内容：read()</h2><p><code>read()</code>系统调用从文件描述符 fd 所指代的打开文件中读取数据。如果调用成功，将返回实际读取的字节数，如果遇到文件结束（EOF）则返回 0，如果出现错误则返回-1。<code>ssize_t</code>数据类型属于有符号的整数，<code>size_t</code>数据类型是无符号的整数类型。</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-c data-lang=c><span style=display:flex><span><span style=color:#75715e>#include</span> <span style=color:#75715e>&lt;unistd.h&gt;</span><span style=color:#75715e>
</span></span></span><span style=display:flex><span><span style=color:#75715e></span>
</span></span><span style=display:flex><span><span style=color:#66d9ef>ssize_t</span> <span style=color:#a6e22e>read</span>(<span style=color:#66d9ef>int</span> fd, <span style=color:#66d9ef>void</span> <span style=color:#f92672>*</span>buffer, <span style=color:#66d9ef>size_t</span> count);
</span></span><span style=display:flex><span><span style=color:#75715e>// Return number of bytes read, 0 on EOF, or -1 on error
</span></span></span></code></pre></div><ul><li>fd：已打开的文件描述符</li><li>buffer：用来存放输入数据的内存缓冲区地址</li><li>count：指定最多能读取的字节数，count 应该小于等于 buffer 的大小</li></ul><h3 id=一个小细节>一个小细节</h3><p>因为<code>read()</code>能够从文件中读取任意序列的字节。有些情况下，输入信息可能是文件数据，但在其他情况下，也有可能是二进制整数或者二进制形式的 C 语言数据结构。<code>read()</code>无从区分这些数据，故而也无法遵从 C 语言对字符串处理的约定—在字符串尾部追加标识字符串结束的空字符。如果输入缓冲区的结尾处需要一个表示终止的空字符，必须显式追加。</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-c data-lang=c><span style=display:flex><span><span style=color:#66d9ef>char</span> buffer[MAX_READ <span style=color:#f92672>+</span> <span style=color:#ae81ff>1</span>];
</span></span><span style=display:flex><span><span style=color:#66d9ef>ssize_t</span> numRead;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span>numRead <span style=color:#f92672>=</span> <span style=color:#a6e22e>read</span>(STDIN_FILENO, buffer, MAX_READ);
</span></span><span style=display:flex><span><span style=color:#66d9ef>if</span> (numRead<span style=color:#f92672>==-</span><span style=color:#ae81ff>1</span>)
</span></span><span style=display:flex><span>    <span style=color:#a6e22e>errExit</span>(<span style=color:#e6db74>&#34;read&#34;</span>);
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span>buffer[numRead] <span style=color:#f92672>=</span> <span style=color:#e6db74>&#39;\0&#39;</span>;
</span></span><span style=display:flex><span><span style=color:#a6e22e>printf</span>(<span style=color:#e6db74>&#34;The input data was: %s</span><span style=color:#ae81ff>\n</span><span style=color:#e6db74>&#34;</span>, buffer);
</span></span></code></pre></div><p>由于表示字符串终止的空字符需要一个字节的内存，所以缓冲区的大小至少要比预计独缺的最大字符串长度多出 1 个字节。</p><h2 id=数据写入文件write>数据写入文件：write()</h2><p><code>write()</code>系统调用将数据写入一个已打开的文件中。如果调用成功，将返回实际写入文件的字节数，该返回值可能小于<code>count</code>参数值。这被称为“部分写”。对于磁盘文件来说，造成“部分写”的原因可能是由于磁盘已满，或是因为进程资源对文件大小的限制。</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-c data-lang=c><span style=display:flex><span><span style=color:#75715e>#include</span> <span style=color:#75715e>&lt;unistd.h&gt;</span><span style=color:#75715e>
</span></span></span><span style=display:flex><span><span style=color:#75715e></span>
</span></span><span style=display:flex><span><span style=color:#66d9ef>ssize_t</span> <span style=color:#a6e22e>write</span>(<span style=color:#66d9ef>int</span> fd, <span style=color:#66d9ef>void</span> <span style=color:#f92672>*</span>buffer, <span style=color:#66d9ef>size_t</span> count);
</span></span><span style=display:flex><span><span style=color:#75715e>// Return number of bytes written, or -1 on error
</span></span></span></code></pre></div><p>对磁盘文件之心 I/O 操作时，<code>write()</code>调用成功并不能保证数据已经写入磁盘。因为为了减少磁盘活动量和加快<code>write()</code>系统调用，内核会缓存磁盘的 I/O 操作。</p><h2 id=关闭文件close>关闭文件：close()</h2><p><code>close()</code>系统调用关闭一个打开的文件描述符，并将其释放回调用进程，供该进程继续使用。当一进程终止时，将自动关闭其已打开的所有文件描述符。</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-c data-lang=c><span style=display:flex><span><span style=color:#75715e>#include</span> <span style=color:#75715e>&lt;unistd.h&gt;</span><span style=color:#75715e>
</span></span></span><span style=display:flex><span><span style=color:#75715e></span>
</span></span><span style=display:flex><span><span style=color:#66d9ef>int</span> <span style=color:#a6e22e>close</span>(<span style=color:#66d9ef>int</span> fd);
</span></span><span style=display:flex><span><span style=color:#75715e>// Return 0 on success, or -1 on error
</span></span></span></code></pre></div><h2 id=改变文件偏移量lseek>改变文件偏移量：lseek()</h2><p>对于每个打开的文件，系统内核会记录其文件偏移量（或者读写偏移量，或者指针）。文件偏移量是指执行下一个<code>read()</code>或<code>write()</code>操作的文件起始位置，会以相对于文件头部起始点的文件当前位置来表示。文件第一个字节的偏移量为 0。</p><p>文件打开时，会将文件偏移量设置为指向文件开始，以后每次<code>read()</code>或<code>write()</code>调用将自动对其进行调整，以指向已读或已写数据后的下一个字节。因此，连续的<code>read()</code>或<code>write()</code>调用将按照顺序递进对文件进行操作。</p><p><code>lseek()</code>系统调用是用来调整这个文件偏移量的。调用成功会返回新的文件偏移量。</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-c data-lang=c><span style=display:flex><span><span style=color:#75715e>#include</span> <span style=color:#75715e>&lt;unistd.h&gt;</span><span style=color:#75715e>
</span></span></span><span style=display:flex><span><span style=color:#75715e></span>
</span></span><span style=display:flex><span><span style=color:#66d9ef>off_t</span> <span style=color:#a6e22e>lseek</span>(<span style=color:#66d9ef>int</span> fd, <span style=color:#66d9ef>off_t</span> offset, <span style=color:#66d9ef>int</span> whence);
</span></span><span style=display:flex><span><span style=color:#75715e>// Return new file offset if successful, or -1 on error
</span></span></span></code></pre></div><ul><li>fd：已打开的文件描述符</li><li>offset：指定一个以字节为单位的数值（SUSv3 规定 <code>off_t</code>数据类型为有符号整型）</li><li>whence：指定按照哪个基准点来解释<code>offset</code>参数</li></ul><h3 id=whence-参数可选值>whence 参数可选值</h3><table><thead><tr><th>参数</th><th>描述</th></tr></thead><tbody><tr><td>SEEK_SET</td><td>将文件偏移量设置为从文件头部起始点开始的 offset 个字节（offset 非负）</td></tr><tr><td>SEEK_CUR</td><td>相对于当前位置将文件偏移量调整 offset 个字节（offset 可正可负）</td></tr><tr><td>SEEK_END</td><td>起始文件偏移量设置在文件尾部，调整 offset 个字节（offset 可正可负）</td></tr></tbody></table><p>附一张书中图：</p><p><figure><img src=http://ww1.sinaimg.cn/large/8d56d744ly1fr7bxnv45cj21ko0mcqa3.jpg alt=whence></figure></p><p><code>lseek()</code>调用只是调整内核中与文件描述符相关的文件偏移量记录，并没有引起对任何物理设备的访问。</p><p><code>lseek()</code>并不使用于所有类型的文件。<strong>不允许将<code>lseek()</code>应用与管道、FIFO、socket 或者终端</strong>。一旦如此，调用将会失败，并将<code>errno</code>置为<strong>ESPIPE</strong>。另一方面，只要合情合理，也可以将<code>lseek()</code>应用于设备。例如，在磁盘或磁带上查找一处具体位置。</p><blockquote><p><code>lseek()</code>调用名中的<code>l</code>源于这样一个事实：offset 参数和调用返回值的类型期初都是<code>long</code>型。早期的 UNIX 系统还提供<code>seek()</code>系统调用，当时这个调用的 offset 和返回值类型为<code>int</code>型。</p></blockquote><h3 id=文件空洞>文件空洞</h3><p>如果程序的文件偏移量已然跨越了文件结尾，然后再执行 I/O 操作，<code>read()</code>调用将返回 0，表示文件结尾。<strong>但！<code>write()</code>函数可以在文件结尾后的任意位置写入数据</strong>。</p><p>从文件结尾后到新写入数据间的这段空间被称为文件空洞。从编程角度看，文件空洞中是存在字节的，读取空洞将返回以 0（空字节）填充的缓冲区。</p><p>然而，文件空洞不占用任何磁盘空间。知道后续某个时间点，在文件空洞中写入了数据，文件系统才会位置分配磁盘块。文件空洞的主要优势在于，于实际需要的空字节分配磁盘快相比，稀疏填充的文件会占用较少的磁盘空间。例如：核心转储（core dump）就是包含空洞文件的常见例子。</p><p>——EOF——</p></div></article></main><aside><div><div><h3>LATEST POSTS</h3></div><div><ul><li><a href=/posts/2025-10-16-kubernetes-apiserver-authorization-mechanism/>Kubernetes APIServer 鉴权机制</a></li><li><a href=/posts/2025-09-30-kubernetes-apiserver-authentication-mechanism/>Kubernetes APIServer 认证机制</a></li><li><a href=/posts/2024-12-16-deploy-kubernetes-by-kubeadm/>使用 kubeadm 搭建 kubernetes 集群</a></li><li><a href=/posts/2024-09-20-how-to-code-review/>如何做code review</a></li><li><a href=/posts/2024-08-12-weakref-in-python/>Python中的弱引用</a></li></ul></div></div></aside><footer><p>Social Links:
<a href=https://github.com/vimiix><b>Github</b></a>.
<a href=https://www.douban.com/people/vimiix/><b>Douban</b></a>.
<a href=mailto:i@vimiix.com><b>Email</b></a>.<br><hr>&copy; 2017-2025
Vimiix Yao; All rights reserved.
<a href=https://beian.miit.gov.cn/>京ICP备19015214号-1</a></p><script src=https://l2dwidget.js.org/lib/L2Dwidget.min.js></script><script>L2Dwidget.init({model:{jsonPath:"https://unpkg.com/live2d-widget-model-tororo@1.0.5/assets/tororo.model.json"}})</script></footer></body></html>